#include "pathtracer.h"

#include <omp.h>
#include "material/light_material.h"
#include "material/object_material.h"
#include "material/color_material.h"
#include "material/texture_material.h"
#include "local_illumination.h"
#include "common/random_generator.h"

namespace Sirat {

namespace Path_Tracing {

Common::Random_Generator random(-1, 1);

Path_Tracer::Path_Tracer(Common::Scene* _scene, Common::Camera* _camera, const unsigned int _path_number, const unsigned int _path_depth) : scene(_scene), camera(_camera), path_number(_path_number), path_depth(_path_depth) {
	image = 0;
	#ifdef _OPENMP
	parallel = false;
	threads_number = 0;
	#endif
}

Path_Tracer::Path_Tracer(const Path_Tracer& _other) {
	scene = _other.scene;
	camera = _other.camera;
	image = _other.image;
	path_number = _other.path_number;
	path_depth = _other.path_depth;

	#ifdef _OPENMP
	parallel = _other.parallel;
	threads_number = _other.threads_number;
	#endif
}

void Path_Tracer::render() {
	Common::Grid* grid = camera->get_grid();
	unsigned short height = grid->get_height();
	unsigned short width = grid->get_width();

	#ifdef _OPENMP
	bool parallel_test = parallel;
	#endif

	int x;
	#ifdef _OPENMP
	if(threads_number > 0) {
		omp_set_num_threads(threads_number);
	}
	#pragma omp parallel for default(shared) private(x) if(parallel_test) schedule(dynamic,10)
	#endif
	for(x = 0;x < width;x++) {
		for(unsigned int y =0;y < height;y++) {
			Math::Ray ray = camera->generate_ray(x,y);

			tuple<Object::Object*,Math::Vector> intersection = scene->nearest_object(ray);
			Object::Object* object = intersection.get<0>();
			Common::Color color;
			if (object != 0) {	 // 0 means no intersection 
				Math::Vector point = intersection.get<1>();
				color = calculate_color(object,ray,point);
			}
			#ifdef _OPENMP
				#pragma omp critical (set_pixel)
			#endif
			{
				if(image != 0) image->set_pixel(x,y,color);
			}
		}
	}
}

Common::Color Path_Tracer::calculate_color(Object::Object* _object, Math::Ray _ray, Math::Vector _point) {
	Common::Color result;
	result = Local_Illumination::calculate_color(_object, _ray, _point, scene);
	Common::Color sum;
	
	for(unsigned int count = 0; count < path_number; count++) {
		Path path = construct_path(_object, _point, path_depth);
		Common::Color path_color = path_illumination(_object, _point, path);
		sum = sum + path_color;
	}

	Material::Object_Material* material = _object->get_material();
	Common::Color diffuse = Common::Color(1.0,1.0,1.0);
	switch(material->get_type()) {
		case Material::Color_Material_Type: {
			diffuse = static_cast<Material::Color_Material*>(material)->get_diffuse();
			break;
		}
		default:
			break;
	}
	result = result + sum*(1.0/(path_number))*diffuse;

	return result;
}

Math::Ray Path_Tracer::get_random_ray(Math::Vector _point, Math::Vector normal) {
	Math::Vector direction;
	do {
		direction.x = random.generate();
		direction.y = random.generate();
		direction.z = random.generate();
	} while(direction.length() > 1.0 || direction.dot_product(normal) < 0.0);

	return Math::Ray(_point, direction.normalize());
}
	
Path Path_Tracer::construct_path(Object::Object* _object, Math::Vector _point, unsigned int _depth) {
	Path result;
	if(_depth == 0) return result;

	Math::Vector normal = _object->get_normal(_point);
	Math::Ray ray = get_random_ray(_point + normal.scalar_product(0.00001), normal);
	
	tuple<Object::Object*, Math::Vector> intersection = scene->nearest_object(ray);
	Object::Object* new_object = intersection.get<0>();
	if(new_object == 0) return result;
	Math::Vector new_point = intersection.get<1>();
	Common::Color local_color = Local_Illumination::calculate_color(new_object, ray, new_point, scene);

	result.push_back(make_tuple(new_object, new_point, local_color, new_point.distance(_point)));

	Path recursive_result = construct_path(new_object, new_point, _depth - 1);
	result.insert(result.end(), recursive_result.begin(), recursive_result.end());

	return result;
}

Common::Color Path_Tracer::path_illumination(Object::Object* _object, Math::Vector _point, Path path) {
	if(path.size() == 0) return Common::Color(0.0,0.0,0.0);

	Common::Color result;
	Path::reverse_iterator iter;
	Path::reverse_iterator last_iter = path.rend();

	for(iter = path.rbegin(); iter != path.rend(); iter++) {
		Object::Object* object = (*iter).get<0>();
		Math::Vector point = (*iter).get<1>();
		Math::Vector normal = object->get_normal(point);

		Material::Object_Material* material = object->get_material();
		Common::Color reflectiveness;
		double distance_factor = 1.0;///(pow((*iter).get<3>(), 2));
		double normal_factor = 1.0;

		if(last_iter != path.rend()) {
			Math::Vector last_point = (*last_iter).get<1>();

			Math::Vector dir = (last_point - point).normalize();
			normal_factor = normal.dot_product(dir);
		}

		switch(material->get_type()) {
			case Material::Color_Material_Type: {
				reflectiveness = static_cast<Material::Color_Material*>(material)->get_reflectiveness();
				break;
			}
			case Material::Texture_Material_Type: {
				reflectiveness = static_cast<Material::Texture_Material*>(material)->get_reflectiveness();
				break;
			}
			default:
				break;
		}

		result = ((*iter).get<2>() + result * reflectiveness * normal_factor) * distance_factor;
		last_iter = iter;
	}

	Math::Vector first_dir = (path[0].get<1>() - _point).normalize();
	Math::Vector first_normal = _object->get_normal(_point);
	double first_normal_factor = first_normal.dot_product(first_dir);

	result = result * first_normal_factor;

	return result;
}

void Path_Tracer::set_output(Common::Image* _image) {
	image = _image;
}

#ifdef _OPENMP
void Path_Tracer::set_parallel(const bool _parallel) {
	parallel = _parallel;
}

void Path_Tracer::set_threads(const unsigned int _threads) {
	threads_number = _threads;
}
#endif

}

}
